Github 仓库地址:https://github.com/Evelynzzz/react-webpack-boilerplate
版本:Webpack 4.39.1
相关依赖:
MiniCssExtractPlugin:0.8.0style-loader:1.0.0less-loader:5.0.0postcss-loader:3.0.0css-loader:3.2.0判断是开发模式还是生产模式在配置 Webpack 时,需要区分用于开发模式还是生产模式。比如我们只需要在生产模式时压缩 CSS;而在开发模式的时候,我们又希望生成 Sourcemap 便于调试,以及样式热更新。那么,怎么在 webpack.config.js 中判断开发、生产模式呢?
我通常会定义三个 webpack 配置文件:
webpack.config.base.js:通用的配置,比如入口,出口,插件,loader等。以下两个配置文件会引入此配置,再修改添加其他配置。webapck.config.dev.js:开发模式下,启动 webpack-dev-server。webapck.config.prod.js:生产模式下,编译打包。然后在 package.json 中分别配置了 start 和 build 脚本:
"scripts": {"start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --open","build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors -p" }注意命令中通过 定义了变量NODE_ENV ,因此在webpack.config.base.js 中可以通过 process.env.NODE_ENV 获取它的值,从而判断时生产模式还是开发模式。
const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式接下来进入正题。
提取 CSS 到单独的文件中在 Webpack 4 之前,我们使用 extract-text-webpack-plugin 插件来提取项目中引入的样式文件,打包到一个单独的文件中。从 Webpack 4 开始,这个插件就过时了,需要使用 MiniCssExtractPlugin。
This plugin extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.
此插件为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。
注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件!接下来会详细说明。
情景一先举一个基础配置的例子。 webpack.config.js:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = { plugins: [new MiniCssExtractPlugin({ filename: '[name].css'}), ], module: {rules: [ {test: /\.css$/,use: [ MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选], },{test: /\.less$/,use: [ MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选], }], },};基于以上配置,如果入口 app.js 中引用了 Root,Root 引入了 Topics。而 Root.js 中引用样式 main.css,Topics.js 中引用了 topics.css。
// 入口文件 app.jsimport Root from './components/Root'// Root.jsimport '../styles/main.less'import Topics from './Topics'// Topics.jsimport "../styles/topics.less"这种情况下,Topics 会和 Root 同属一个 chunk,所以会一起都打包到 app.js 中, 结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css。而不是生成两个 css 文件。
AssetSize ChunksChunk Names app.css 332 bytes1 [emitted] appapp.js283 KiB1 [emitted] [big] app情景二但是,如果 Root.js 中并没有直接引入 Topics 组件,而是配置了代码分割 ,比如模块的动态引入,那么结果就不一样了:
AssetSize ChunksChunk Names app.css 260 bytes1 [emitted] appapp.js281 KiB1 [emitted] [big] app topics.bundle.js2.55 KiB4 [emitted] topicstopics.css72 bytes4 [emitted] topics因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。这才是“为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件”的真正含义。
情景三但是,如果分割了 chunk,还是只希望只生成一个 CSS 文件怎么办呢?也是可以做到的。但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroups。
optimization.splitChunks 是干什么的呢?在 Webpack 4 以前,我们使用 CommonsChunkPlugin 来提取重复引入的第三方依赖,比如把 React 和 Jquery 单独提取到一个文件中。而从 Webpack 4 开始,CommonsChunkPlugin 被 optimization.splitChunks 替代了。从命名也能看出来,它是用来拆分 chunk 的。怎么在这里需要用到这个配置呢?先来看看配置怎么写的:
optimization: { splitChunks: {cacheGroups: { // Extracting all CSS/less in a single file styles: {name: 'styles',test: /\.(c|le)ss$/,chunks: 'all',enforce: true, },} }},打包结果:
AssetSize ChunksChunk Namesapp.js281 KiB2 [emitted] [big] app styles.bundle.js 402 bytes0 [emitted] stylesstyles.css 332 bytes0 [emitted] styles topics.bundle.js2.38 KiB5 [emitted] topics可以看出,样式确实都被提取到一个 styles.css 文件中了。但与此同时多了一个 style.bundle.js 文件,这就是 optimization.splitChunks.cacheGroups 的效果。具体原理就不在此深究,感兴趣的话可以研究一下。
MiniCssExtractPlugin vs. style-loader首先这两个插件用途完全不同:MiniCssExtractPlugin 提取 JS 中引入的 CSS 打包到单独文件中,然后通过标签 添加到头部;style-loader 则是通过 标签直接将 CSS 插入到 DOM 中。
通常,基本的 CSS 配置都是类似这样的。先 style-loader,然后 css-loader。
module: {rules: [{test: /\.css$/,use: ['style-loader', 'css-loader'],},],}但后来由于想要提取 CSS 到单独的文件里,就需要用上 MiniCssExtractPlugin。那么问题来了,如下的配置可行吗?
{test: /\.css$/,use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader','postcss-loader'],}生产模式根据 MiniCssExtractPlugin 文档 中说到的,此插件适用于没有style-loader 的生产模式中,以及需要 HMR 的开发模式。
This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.
也就是说,在生产模式中,以上的配置同时使用了style-loader 和 MiniCssExtractPlugin 是不合适的(试了一下,style-loader不会起作用)。
我们只能取其一。也可以如下两者结合,开发模式中使用 style-loader,生产模式中使用 MiniCssExtractPlugin。各取所需,毕竟这两者的作用还是很不同。
{test: /\.css$/,use: [devMode?'style-loader':MiniCssExtractPlugin.loader,'css-loader','postcss-loader']}样式文件热更新(HMR)从上面引用的那句话也可以看出,在开发模式中, 我们可以用 MiniCssExtractPlugin 实现样式的 HMR(Hot Module Replacement,模块热更新)。
样式文件的 HMR 是指什么呢?如果没有配置 HMR,开发模式下,修改 CSS 源文件的时候,页面并不会自动刷新加载修改后的样式。需要手动刷新页面,才会加载变化。而 HMR 实现了被修改模块的热更新,使得变化即时显示在页面上,不再需要刷新整个页面。
但其实 style-loader也实现了 HMR 接口,如 Wepack 文档的 In a Module 中说到的:
HMR is an opt-in feature that only affects modules containing HMR code. One example would be patching styling through the style-loader. In order for patching to work, the style-loader implements the HMR interface; when it receives an update through HMR, it replaces the old styles with the new ones.
因此开发环境下,这两个插件都是可以热更新 CSS 的,只是 MiniCssExtractPlugin 的配置可能更丰富一些。比如说:style-loader 只热更新 JS 中引入的样式,如果 index.html 中通过 引入了服务器中的一个CSS 文件:
如果开发模式下,修改 test.css 的源码,style-loader 不会热更新变化 CSS,而是需要刷新整个页面,但 MiniCssExtractPlugin 则会自动重新加载所有的样式。可能还有其他区别,在此不详细说明了。
MiniCssExtractPlugin 插件可以这么配置 Less 文件的 HMR:
const devMode = process.env.NODE_ENV === 'development'; // 是否是开发模式//......module.exports = {//......module: { rules:[{ test: /\.less$/i, use: [{ loader: MiniCssExtractPlugin.loader, options: {// 只在开发模式中启用热更新hmr: devMode,// 如果模块热更新不起作用,重新加载全部样式reloadAll: true, },},'css-loader','postcss-loader','less-loader' ]},// ...... ]}}参考阅读MiniCssExtractPlugin 文档: https://webpack.js.org/plugins/mini-css-extract-plugin/Webpack 文档之Loading CSS: https://webpack.js.org/guides/asset-management/#loading-cssstyle-loader 文档:https://webpack.js.org/loaders/style-loader/Webpack 文档之Hot Module Replacement: https://webpack.js.org/concepts/hot-module-replacement/